﻿using System;
using System.Linq;
using System.Collections.ObjectModel;
using System.IO.IsolatedStorage;
using System.IO;
using System.Xml.Serialization;
using SilverlightDatabase.Resources;
using SilverlightDatabase.Exceptions;
using SilverlightDatabase.Core;
using System.Collections.Generic;
using System.Xml;
using SilverlightDatabase.Compression;
using System.ServiceModel.DomainServices.Client;
using Utils;
using System.Runtime.Serialization;
using System.Diagnostics;

namespace SilverlightDatabase
{
    /// <summary>
    /// Represents a table that contains rows of type T
    /// </summary>
    /// <typeparam name="T">Row type</typeparam>
    public class Table<T> : ObservableCollection<T>, ITable
    {
        private IDatabase _database;
        private bool _raiseCollectionChangedEvents = true;
        private byte[] _certificate = new byte[0];
        private bool _useCompression = false;
        public event EventHandler OnDirty;
        public event IgnoreTypeHandler IgnoreType;

        /// <summary>
        /// Constror for table - use Database.CreateTable instead
        /// </summary>
        [Obsolete("Do not use constructor - use Database.CreateTable() instead")]
        public Table() { }

        private Table(IDatabase database, byte[] certificate, bool useCompression)
        {
            _database = database;
            _certificate = certificate;
            _useCompression = useCompression;
        }

        /// <summary>
        /// Set internal variables data when a table is de-serialized
        /// </summary>
        /// <param name="database">Database name that table belongs to</param>
        /// <param name="certificate">certificate to use for encryption</param>
        /// <param name="useCompression">Use compression for storage</param>
        public void Initialize(IDatabase database, byte[] certificate, bool useCompression)
        {
            _database = database;
            _certificate = certificate;
            _useCompression = useCompression;
        }

        /// <summary>
        /// Create new table for specified database
        /// </summary>
        /// <param name="database">Database name that table belongs to</param>
        /// <param name="certificate">certificate to use for encryption</param>
        /// <param name="useCompression">Use compression for storage</param>
        /// <returns></returns>
        internal static Table<T> CreateTable(IDatabase database, byte[] certificate, bool useCompression)
        {
            return new Table<T>(database, certificate, useCompression);
        }

        public long GetSpaceRequirements()
        {
            return this.GetSpaceRequirements(null);
        }

        /// <summary>
        /// Save table to isolated storage
        /// </summary>
        /// <exception cref="SilverlightDatabase.Exceptions.SaveException">Thrown when an error currs during save</exception>
        public long GetSpaceRequirements(XmlAttributeOverrides overrides)
        {
            if (_database == null || !string.IsNullOrEmpty(_database.DatabaseName))
            {
                try
                {
                    using (var stream = new MemoryStream())
                    {
                        if (overrides == null)
                        {
                            return WriteTableToStreamForSpace(stream);
                        }
                        else
                        {
                            return WriteTableToStreamForSpace(stream, overrides);
                        }
                    }
                }
                catch (Exception ex)
                {
                    throw new SaveException(ex);
                }

            }
            else
            {
                throw new TableCannotBeSavedException(DatabaseResources.TableWithoutDatabaseCannotBeSaved);
            }

        }

        public long WriteTableToStreamForSpace(Stream stream)
        {
            return this.WriteTableToStreamForSpace(stream, null);
        }

        /// <summary>
        /// Write content of a table to a stream
        /// </summary>
        /// <param name="stream">Stream to write table to</param>
        public long WriteTableToStreamForSpace(Stream stream, XmlAttributeOverrides overrides)
        {
            string content = string.Empty;
            long spaceNeeded = 0;

            using (StringWriter stringWriter = new StringWriter())
            {
                XmlSerializer serializer;

                overrides = GetTypeOverrides(ref overrides);

                if (overrides == null)
                {
                    serializer = new XmlSerializer(this.GetType());
                }
                else
                {
                    serializer = new XmlSerializer(this.GetType(), overrides);
                }

                serializer.Serialize(stringWriter, this);
                stringWriter.Flush();
                content = stringWriter.GetStringBuilder().ToString();

                if (_certificate.Length > 0)
                {
                    content = Cryptography.Encrypt(content, Convert.ToBase64String(_certificate));
                }

                stringWriter.Close();
            }

            if (_useCompression)
            {
                using (System.IO.BinaryWriter writer = new BinaryWriter(stream))
                {
                    writer.Write(CompressionUtility.CompressString(content));
                    writer.Flush();

                    spaceNeeded = stream.Length;

                    writer.Close();
                }
            }
            else
            {
                using (StreamWriter writer = new StreamWriter(stream))
                {
                    writer.Write(content);
                    writer.Flush();

                    spaceNeeded = stream.Length;

                    writer.Close();
                }
            }

            return spaceNeeded;
        }

        public void Save()
        {
            this.Save(null);
        }

        /// <summary>
        /// Save table to isolated storage
        /// </summary>
        /// <exception cref="SilverlightDatabase.Exceptions.SaveException">Thrown when an error currs during save</exception>
        public void Save(XmlAttributeOverrides overrides)
        {
            if (_database == null || !string.IsNullOrEmpty(_database.DatabaseName))
            {
                try
                {
                    using (IsolatedStorageFile store = IsolatedStorageFile.GetUserStoreForSite())
                    {
                        var spaceRequirements = GetSpaceRequirements(overrides);

                        if (store.AvailableFreeSpace < spaceRequirements)
                        {
                            if (!store.IncreaseQuotaTo((long)(store.Quota + (_database.GrowthSize * Math.Pow(2, 10)))))
                            {
                                throw new OutOfMemoryException("User refused to increase usage");
                            }
                        }

                        string fileName = FileNameMaker.Make<T>(_database.DatabaseName);

                       //  _database.Log.InfoFormat("Table of type '{0}' saving to file: '{1}'.", typeof(T).FullName, fileName);

#if SILVERLIGHT
                        if (store.FileExists(fileName))
                        {
                            store.DeleteFile(fileName);
                        }
#else
                        if (store.GetFileNames(fileName).Length > 0)
                        {
                            store.DeleteFile(fileName);
                        }
#endif
                        using (IsolatedStorageFileStream stream = new IsolatedStorageFileStream(fileName, FileMode.OpenOrCreate, store))
                        {
                            if (overrides == null)
                            {
                                WriteTableToStream(stream);
                            }
                            else
                            {
                                WriteTableToStream(stream, overrides);
                            }
                        }
                    }

                    if (OnDirty != null)
                    {
                        OnDirty(this, EventArgs.Empty);
                    }
                }
                catch (Exception ex)
                {
                    throw new SaveException(ex);
                }

            }
            else
            {
                throw new TableCannotBeSavedException(DatabaseResources.TableWithoutDatabaseCannotBeSaved);
            }

        }

        public void WriteTableToStream(Stream stream)
        {
            this.WriteTableToStream(stream, null);
        }

        /// <summary>
        /// Write content of a table to a stream
        /// </summary>
        /// <param name="stream">Stream to write table to</param>
        public void WriteTableToStream(Stream stream, XmlAttributeOverrides overrides)
        {
            string content = string.Empty;

            using (StringWriter stringWriter = new StringWriter())
            {
                XmlSerializer serializer;

                overrides = GetTypeOverrides(ref overrides);

                if (overrides == null)
                {
                    serializer = new XmlSerializer(this.GetType());
                }
                else 
                {
                    serializer = new XmlSerializer(this.GetType(), overrides);
                }

                serializer.Serialize(stringWriter, this);
                stringWriter.Flush();
                content = stringWriter.GetStringBuilder().ToString();

                if (_certificate.Length > 0)
                {
                    content = Cryptography.Encrypt(content, Convert.ToBase64String(_certificate));
                }
                stringWriter.Close();
            }

            if (_useCompression)
            {
                using (System.IO.BinaryWriter writer = new BinaryWriter(stream))
                {
                    writer.Write(CompressionUtility.CompressString(content));
                    writer.Flush();
                    writer.Close();
                }
            }
            else
            {
                using (StreamWriter writer = new StreamWriter(stream))
                {
                    writer.Write(content);
                    writer.Flush();
                    writer.Close();
                }
            }
        }

        private XmlAttributeOverrides GetTypeOverrides(ref XmlAttributeOverrides overrides)
        {
            return GetTypeOverrides(ref overrides, typeof(T));
        }

        private XmlAttributeOverrides GetTypeOverrides(ref XmlAttributeOverrides overrides, Type type)
        {
            if (type != typeof(object) && !type.IsSerializable())
            {
                foreach (var fieldInfo in type.GetFields())
                {
                    if (fieldInfo.IsPublic)
                    {
                        var attributes = new XmlAttributes();
                        var fieldType = fieldInfo.FieldType;

                        if (!fieldType.IsSerializable())
                        {
                            bool ignore = true;

                            if (IgnoreType != null)
                            {
                                IgnoreType(fieldType, ref ignore);
                            }

                            if (ignore)
                            {
                                attributes.XmlIgnore = true;

                                if (overrides == null)
                                {
                                    overrides = new XmlAttributeOverrides();
                                }

                                overrides.Add(type, fieldInfo.Name, attributes);
                            }
                            else
                            {
                                GetTypeOverrides(ref overrides, fieldType);
                            }
                        }
                    }
                }

                foreach (var propertyInfo in type.GetProperties())
                {
                    if (propertyInfo.GetGetMethod().IsPublic)
                    {
                        var attributes = new XmlAttributes();
                        var propertyType = propertyInfo.PropertyType;

                        if (!propertyType.IsSerializable())
                        {
                            bool ignore = true;

                            if (IgnoreType != null)
                            {
                                IgnoreType(propertyType, ref ignore);
                            }

                            if (ignore)
                            {
                                attributes.XmlIgnore = true;

                                if (overrides == null)
                                {
                                    overrides = new XmlAttributeOverrides();
                                }

                                overrides.Add(type, propertyInfo.Name, attributes);
                            }
                            else
                            {
                                GetTypeOverrides(ref overrides, propertyType);
                            }
                        }
                    }
                }
            }

            return overrides;
        }

        /// <summary>
        /// Raises collection changed event
        /// </summary>
        /// <param name="e">Event args for collection changed event</param>
        protected override void OnCollectionChanged(System.Collections.Specialized.NotifyCollectionChangedEventArgs e)
        {
            if (OnDirty != null)
            {
                OnDirty(this, EventArgs.Empty);
            }

            if (_raiseCollectionChangedEvents)
            {
                base.OnCollectionChanged(e);
            }
        }

        /// <summary>
        /// Add a range of items to the table
        /// </summary>
        /// <param name="range">Range of items</param>
        public void AddRange(IEnumerable<T> range)
        {
            _raiseCollectionChangedEvents = false;
            try
            {
                foreach (var item in range)
                {
                    Add(item);
                }
            }
            finally
            {
                _raiseCollectionChangedEvents = true;
                OnCollectionChanged(new System.Collections.Specialized.NotifyCollectionChangedEventArgs(System.Collections.Specialized.NotifyCollectionChangedAction.Reset));
            }
        }

        /// <summary>
        /// Remove a range of items from the table
        /// </summary>
        /// <param name="range">Range of items to remove</param>
        public void RemoveRange(IEnumerable<T> range)
        {
            _raiseCollectionChangedEvents = false;
            try
            {
                foreach (var item in range)
                {
                    Remove(item);
                }
            }
            finally
            {
                _raiseCollectionChangedEvents = true;
                OnCollectionChanged(new System.Collections.Specialized.NotifyCollectionChangedEventArgs(System.Collections.Specialized.NotifyCollectionChangedAction.Reset));
            }
        }

        /// <summary>
        /// Delete a range of items from the table based on
        /// condtion being met
        /// </summary>
        /// <param name="deleteCondition">
        /// Condition that must be met to delete the items
        /// </param>
        public void RemoveRange(Func<T, bool> deleteCondition)
        {
            var deletedItems = (from oneItem in this
                                where deleteCondition(oneItem)
                                select oneItem);
            if (deletedItems != null && deletedItems.Count() > 0)
            {
                RemoveRange(deletedItems.ToArray());
            }
        }


        #region ITable Members

        /// <summary>
        /// Type of objec that this table contains
        /// </summary>
        public Type RowType
        {
            get { return typeof(T); }
        }

        #endregion

    }
}
